.intel_syntax noprefix

#include "../apocconfig.h"
#include "../x86/apocx86.h"

.set QEMU, 1

# Disk I/O configuration
.if !QEMU
.set PORT_DISK_IO, 0
.else
.set PORT_DISK_IO, 1
.endif

# GDT segment selectors
.set GDT32_CSEG_SEL, 8
.set GDT32_DSEG_SEL, 16

.set GDT64_DSEG_SEL, 8
.set GDT64_CSEG_SEL, 16

# Protected mode enable flag
.set CR0_PE_ON, 1

.set KERNEL_MAIN_ADDRESS, 0x100000
.set KERNEL_NUM_SECTORS, 64
.if !QEMU
.set KERNEL_START_SECTOR, 0x1B4B3800
.else
.set KERNEL_START_SECTOR, 1
.endif


#
# Procedure
# 2. Enable A20 line
# 1. If x86-64:
#       a) Build page tables
# 3. Load x86-32 GDT
# 4. Set PE bit in CR0
# 2. JMP to x86-32 (protected) mode
# 4. If x86-64:
#   a) Enable PAE bit in CR4
#   b) Copy address of PML4 to CR4
#   c) Load x86-64 GDT
#   d) Switch to x86-64 mode
# 5. Call KernelMain()

.globl start
start:
  .code16 # Assemble for 16-bit mode
  cli     # Disable interrupts
  cld     # String operations add to edi

  # Zero the important data segment registers (DS, ES, SS).
  xor ax, ax
  mov ss, ax
  mov ds, ax
.if !PORT_DISK_IO
  mov ax, 0xFFFF # High memory
.endif
  mov es, ax

  # Enable A20:
  #   For backwards compatibility with the earliest PCs, physical
  #   address line 20 is tied low, so that addresses higher than
  #   1MB wrap around to zero by default.  This code undoes this.
seta20.1:
  in al, 0x64   # Wait for not busy
  test al, 0x2
  jnz seta20.1

  mov al, 0xd1  # 0xd1 -> port 0x64
  out 0x64, al 

seta20.2:
  in al, 0x64   # Wait for not busy
  test al, 0x2
  jnz seta20.2

  mov al, 0xdf  # 0xdf -> port 0x60
  outb 0x60, al

  .if !PORT_DISK_IO
  call load_kernel
  .endif
  
  # Switch from real to protected mode, using a bootstrap x86_32_gdt
  # and segment translation that makes virtual addresses 
  # identical to their physical addresses, so that the 
  # effective memory map does not change during the switch.
  lgdt x86_32_gdt_desc
  mov eax, cr0
  or eax, CR0_PE_ON
  mov cr0, eax
  
  # Jump to next instruction, but in 32-bit code segment.
  # Switches processor into 32-bit mode.
  jmp GDT32_CSEG_SEL, x86_32_code

  .code32 # Assemble for 32-bit mode
x86_32_code:
  # Set up the protected-mode data segment registers
  mov ax, GDT32_DSEG_SEL    # Our data segment selector
  mov ds, ax                # -> DS: Data Segment
  mov es, ax                # -> ES: Extra Segment
  mov fs, ax                # -> FS
  mov gs, ax                # -> GS
  mov ss, ax                # -> SS: Stack Segment
  
  # Initialize stack 
  mov ebp, 0x003FFFFF
  mov esp, ebp

  .if PORT_DISK_IO
  call load_kernel
  .endif

.if APOC_ARCH == APOC_ARCH_X86_64
  # Build flat 2MB paging system (consequtive in memory)
  # [00008000 - 00009000] (4096 B) - PML4 *ALIGN4KB
  # [00009000 - 0000A000] (4096 B) - PDPT *ALIGN4KB
  # [0000A000 - 0000B000] (4096 B) - PD *ALIGN4KB

  mov edi, 0x00008000   # Start of tables
  cld                   # String operations add to edi 

  # Build PML4
  # PHYSADDR-PDPT = 0x9000 =                              10010000 00000000
  # PML4E0 = PHYSADDR-PDPT | 1111 =
  # 00000000 00000000 00000000 00000000 00000000 00000000 10010000 00001111

  # First entry is valid
  mov eax, 0b00000000000000001001000000001111
  stosd

  # Write rest of PML4 to be NULL
  xor eax, eax
  mov ecx, 1023
  rep stosd

  # Build PDPT

  # PHYSADDR-PD = 0xA000 =                                10100000 00000000
  # PDPTE0 = PHYSADDR-PD | 1111 =
  # 00000000 00000000 00000000 00000000 00000000 00000000 10100000 00001111

  # First entry is valid
  mov eax, 0b00000000000000001010000000001111
  stosd

  # Write rest of PDPT to be NULL
  xor eax, eax
  mov ecx, 1023
  rep stosd

  # Build flat PD

  # PHYSADDR-PAGE = 0 - 0x200000*N
  # PDE0 = PHYSADDR-PAGE | 110001111 =
  # 00000000 00000000 00000000 00000000 00000000 00000000 00000001 10001111
  sub edi, 8
  mov ecx, -0x200000

write_pde:
  add edi, 8                                  # Next PDE
  add ecx, 0x200000                           # Increment page address by 2 MB

  mov eax, 0b00000000000000000000000110001111 # Build source PDE
  or eax, ecx

  mov DWORD PTR [edi], eax                    # Write PDE
  mov DWORD PTR [edi + 4], 0

  cmp edi, 0x0000A000 + 8*511                 # End of PD?
  jne write_pde                               # Not end of PD

  # Enable physical-address extensions (PAE)
  mov eax, cr4
  or eax, 0b100000
  mov cr4, eax

  # Point cr3 at PML4
  mov eax, 0x8000
  mov cr3, eax

  # Enable long mode (set EFER.LME=1)
  mov ecx, 0xC0000080                         # EFER MSR number
  rdmsr                                       # Read EFER
  or eax, 0b100000001                         # LME amd SYSCALL/SYSRET
  wrmsr                                       # Write EFER
  
  # Enable paging AND protected mode to activate long mode (set CR0.PG=1)
  mov eax, cr0                                # Read CR0
  or eax, 0b10000000000000000000000000000001  # enable paging + pmode
  mov cr0, eax                                # Write CR0

  lgdt x86_64_gdt_desc

  jmp GDT64_CSEG_SEL, x86_64_code

  .code64 # Assemble for x86_64 mode
x86_64_code:

  # Set up the stack pointer 
  mov rbp, 0x00000000003FFFFF
  mov rsp, rbp

  # Call into kernel
  call KERNEL_MAIN_ADDRESS

stop:
  jmp stop

.else # CPU_X86_32
  .code32

  # Call into kernel
  call KERNEL_MAIN_ADDRESS

stop:
  jmp stop

.endif

#
# FUNCTIONS
#
.if PORT_DISK_IO
.code32

.globl wait_disk
wait_disk:
  # 1F7 - Disk status
  mov edx, 0x1f7
wait:
  in  al, dx
  and eax, 0xc0
  cmp eax, 0x40
  jne wait

  ret

.globl load_kernel
load_kernel:
  call wait_disk

  # 1F2 - Sector count
  mov edx, 0x01f2
  mov al, KERNEL_NUM_SECTORS
  out dx, al

  # 1F3 - Sector number
  mov dl, 0xf3
  mov al, KERNEL_START_SECTOR + 1
  out dx, al

  # 1F4 - Cylinder low
  mov dl, 0xf4
  xor al, al
  out dx, al

  # 1F5 - Cylinder high
  mov dl, 0xf5
  out dx, al

  # 1F6 - Drive/Head
  mov dl, 0xf6
  mov al, 0x00a0 # Drive = 0, head = 0
  out dx, al

  # 1F7 - Command port
  mov dl, 0xf7
  mov al, 0x20   # Read with retry
  out dx, al

  call wait_disk

  # 1F0 - Data register (R/W)
  mov dl, 0xf0
  mov ecx, (KERNEL_NUM_SECTORS*512) / 4 # ecx - num inputs (one sector (512 bytes) 4 bytes at a time
  mov edi, KERNEL_MAIN_ADDRESS
  cld
  repnz ins DWORD PTR es:[edi], dx

  ret
.else
.code16

.globl load_kernel
load_kernel:
read_sectors:
  mov si, offset dap # Disk address packet (DS:SI)
  mov dl, 0x80       # Drive number (0x80 for hard disk)
  mov ah, 0x42       # Function selector (AH = 42h Extended Read)
  int 0x13
  jc read_sectors    # CF flag set on failure

  ret

.endif

#
# DATA
#

.if !PORT_DISK_IO

dap:
# Disk address packet
.byte 0x10                    # Size of packet
.byte 0                       # Unused
.byte KERNEL_NUM_SECTORS      # Num sectors
.byte 0                       # Unused
.word 0x10                    # Offset
.word 0xFFFF                  # Segment
.quad KERNEL_START_SECTOR + 1 # Sector start

.endif

# Flat x86-32 GDT
.p2align 2
x86_32_gdt:
  SEG_NULL                            # null seg
  SEG32(STA_X|STA_R, 0x0, 0xFFFFFFFF) # code seg
  SEG32(STA_W, 0x0, 0xFFFFFFFF)       # data seg

# x86-32 GDT descriptor
x86_32_gdt_desc:
  .word 8*3 - 1                       # sizeof(x86_32_gdt) - 1
  .long x86_32_gdt                    # address x86_32_gdt

.if APOC_ARCH == APOC_ARCH_X86_64

# Flat x86-64 GDT
.p2align 3
x86_64_gdt:
  SEG_NULL                            # null seg
  SEG64(STA_W, 0x0, 0xFFFFFFFF)       # data seg
  SEG64(STA_X|STA_R, 0x0, 0xFFFFFFFF) # code seg

# x86-64 GDT descriptor
x86_64_gdt_desc:
  .word 8*3 - 1                       # sizeof(x86_64_gdt) - 1
  .quad x86_64_gdt                    # address x86_64_gdt

.endif
